﻿using System;

using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Drawing.Imaging;
using System.Diagnostics;
using Silvermoon.OpenGL.Native;
using Silvermoon.OpenGL.Native.Fonts;
using System.IO;

namespace Silvermoon.OpenGL
{
    /// <summary>
    /// Holds a <see cref="Font"/> in a texture for fast rendering in conjunction with <see cref="TextureString"/>.
    /// </summary>
    public partial class FontSprite : IDisposable
    {
        #region structs

        public class CharData
        {
            public short Width;
            public short X;
            public short Y;

            public int Right { get { return X + Width; } }

            public static readonly CharData Empty = new CharData();
        }

        struct tagTextMetric
        {
            public int tmHeight;
            public int tmAscent;
            public int tmDescent;
            public int tmInternalLeading;
            public int tmExternalLeading;
            public int tmAveCharWidth;
            public int tmMaxCharWidth;
            public int tmWeight;
            public int tmOverhang;
            public int tmDigitizedAspectX;
            public int tmDigitizedAspectY;
            public byte tmFirstChar;
            public byte tmLastChar;
            public byte tmDefaultChar;
            public byte tmBreakChar;
            public byte tmItalic;
            public byte tmUnderlined;
            public byte tmStruckOut;
            public byte tmPitchAndFamily;
            public byte tmCharSet;
        };


        const int StartCharIndex = 0;
        const int EndCharIndex = 255;

        #endregion
        #region ctor/dtor

        static FontSprite()
        {
            StringBuilder sb = new StringBuilder();
            for (int c = 32; c < 255; c++)
            {
                sb.Append((char)c);
            }
            sb.Append(@"•€√±≠≤≥÷∞µ₤●π");  // ∆∏∑∂№℅ …‹›•
            DefaultCharacters = sb.ToString();
        }


        private FontSprite(Font font, bool shadowed) : this(font, DefaultCharacters, shadowed) { }

        private FontSprite(Font font) : this(font, DefaultCharacters, false) { }

        private FontSprite(Font font, string charMap, bool shadowed)
            : base()
        {
            SetFont(font);
            refCount = 1;
            IsShadowed = shadowed;

            GetFontMetrics(font, charMap);
            texture = new Texture();

            RenderBitmapAndFillCharArray(texture, font);
        }


        public FontSprite(GlFontFamily fontFamily, int size, FontStyle style, bool shadowed)
            : this(FontUtil.FromData(fontFamily, size, style), shadowed)
        {
        }

        public FontSprite(GlFontFamily fontFamily, int size, FontStyle style, bool shadowed, string charMap)
            : this(FontUtil.FromData(fontFamily, size, style), charMap, shadowed)
        {
        }


        public void Dispose()
        {
            if (--refCount <= 0)
            {
                if (key != 0) glFonts.Remove(key);
                key = 0;
                if (texture != null) texture.Dispose();
                texture = null;
                textureArrayPointer = null;
                Characters.Clear();
            }
            GC.SuppressFinalize(this);
        }

        ~FontSprite()
        {
            refCount = 0;
            Dispose();
        }

        #endregion
        #region fields

        private static bool isCacheEnabled = true;
        private int key;
        private int refCount;
        private static FontSprite regularFont;
        private static FontSprite boldFont;
        private static FontSprite regularShadowedFont;
        private static FontSprite boldShadowedFont;
        private int ellipsisWidth = 0;

        /// <summary>
        /// Specifies all characters that are used for new fonts by default if not explicitely specified in the constructor.
        /// </summary>
        public static readonly string DefaultCharacters;

        private readonly static Bitmap fontBitmap = new Bitmap(1, 1, PixelFormat.Format16bppRgb565);
        private readonly static Graphics fontGraphics = Graphics.FromImage(fontBitmap);
        private Texture texture;

        private short[] textureArrayPointer;

        private Dictionary<char, CharData> Characters = new Dictionary<char, CharData>();
        #endregion
        #region props

        public bool IsDefault
        {
            get { return this == regularFont; }
        }

        /// <summary>
        /// Gets or sets whether file caching is enabled.
        /// It set to true, FontSprite.FromData checks if a cached file is in the APPFOLDER\FontCache folder and reads the data for the font from this file instead of caculating and rendering
        /// the data. Otherwise the font is created and then the file is written to the APPFOLDER\FontCache folder.
        /// If set to false, neither cache lookup nor cache writeback is performed which results in worse performance.
        /// </summary>
        public static bool IsCacheEnabled
        {
            get { return isCacheEnabled; }
            set { isCacheEnabled = value; }
        }

        /// <summary>
        /// Gets the name of the font.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// Gets the style of the font.
        /// </summary>
        public FontStyle Style { get; private set; }

        /// <summary>
        /// Gets the size of the font.
        /// </summary>
        public float Size { get; private set; }

        /// <summary>
        /// Gets how many characters are mapped.
        /// </summary>
        public int MappedCharacters { get { return Characters != null ? Characters.Count : 0; } }



        /// <summary>
        /// Gets the width of the ellipsis aka "..." characters.
        /// </summary>
        public int EllipsisWidth
        {
            get
            {
                if (ellipsisWidth == 0) ellipsisWidth = MeasureString("...").Width;
                return ellipsisWidth;
            }
        }
        public static int DefaultFontHeight = 11;

        /// <summary>
        /// Gets whether the font is shadowed.
        /// </summary>
        public bool IsShadowed { get; private set; }

        /// <summary>
        /// Gets the default font.
        /// </summary>
        public static FontSprite Regular
        {
            get
            {
                if (regularFont == null)
                {
                    regularFont = FontSprite.FromData(GlFontFamily.GenericSansSerif, DefaultFontHeight, FontStyle.Regular, false);
                }
                return regularFont;
            }
        }

        /// <summary>
        /// Gets the default bold font.
        /// </summary>
        public static FontSprite Bold
        {
            get
            {
                if (boldFont == null)
                {
                    boldFont = FromData(GlFontFamily.GenericSansSerif, DefaultFontHeight, FontStyle.Bold, false);
                }
                return boldFont;
            }
        }

        /// <summary>
        /// Gets the default regular font with a drop shadow.
        /// Note that this type of font requires more memory and is also slower to render, but faster as if a StringShape has set Shadowed to true but is using no shadowed font.
        /// </summary>
        public static FontSprite RegularShadowed
        {
            get
            {
                if (regularShadowedFont == null)
                {
                    regularShadowedFont = FromData(GlFontFamily.GenericSansSerif, DefaultFontHeight, FontStyle.Regular, true);
                }
                return regularShadowedFont;
            }
        }

        /// <summary>
        /// Gets the default bold font with a drop shadow.
        /// Note that this type of font requires more memory and is also slower to render, but faster as if a StringShape has set Shadowed to true but is using no shadowed font.
        /// </summary>
        public static FontSprite BoldShadowed
        {
            get
            {
                if (boldShadowedFont == null)
                {
                    boldShadowedFont = FromData(GlFontFamily.GenericSerif, DefaultFontHeight, FontStyle.Bold, true);
                }
                return boldShadowedFont;
            }
        }

        public Size TextureSize { get; private set; }
        public int CharHeight { get; private set; }
        public int MaxCharWidth { get; private set; }

        internal int TrailingSpace { get; private set; }
        internal int LeadingSpace { get; private set; }

        public Texture Texture { get { return texture; } }

        /// <summary>
        /// Gets triples of left/top/width values for each character.
        /// </summary>
        public short[] TextureArrayPointer { get { return textureArrayPointer; } }

        #endregion
        #region methods
        [DllImport("coredll")]
        extern static bool GetCharWidth32(IntPtr hdc, int iFirstChar, int iLastChar, [Out] int[] lpBuffer);

        [DllImport("coredll")]
        extern static IntPtr SelectObject(IntPtr hdc, IntPtr obj);

        [DllImport("coredll")]
        extern static bool GetTextMetrics(IntPtr hdc, ref tagTextMetric lptm);

        private void GetFontMetrics(Font font, string charMap)
        {
            using (Bitmap bm = new Bitmap(1, 1))
            {
                using (Graphics fontGraphics = Graphics.FromImage(bm))
                {
                    IntPtr hfont = font.ToHfont();
                    IntPtr hdc = fontGraphics.GetHdc();
                    tagTextMetric metrics = new tagTextMetric();

                    int[] charWidths = new int[EndCharIndex - StartCharIndex + 1];
                    int[] charWidths2 = new int[4];
                    try
                    {
                        SelectObject(hdc, hfont);
                        if (!GetCharWidth32(hdc, StartCharIndex, EndCharIndex, charWidths)) throw new SystemException("Failure while measuring character widths.");


                        foreach (char c in charMap)
                        {
                            if (this.Characters.ContainsKey(c)) continue;
                            if (c >= StartCharIndex && c <= EndCharIndex)
                            {
                                this.Characters.Add(c, new CharData { Width = (short)charWidths[c] });
                            }
                            else
                            {
                                if (!GetCharWidth32(hdc, (int)c, ((int)c) + 0, charWidths2)) throw new SystemException("Failure while measuring character widths.");
                                this.Characters.Add(c, new CharData { Width = (short)charWidths2[0] });
                            }
                        }
                        GetTextMetrics(hdc, ref metrics);
                    }
                    finally
                    {
                        fontGraphics.ReleaseHdc(hdc);
                    }
                    CharHeight = metrics.tmHeight;
                    MaxCharWidth = metrics.tmMaxCharWidth;
                    TrailingSpace = metrics.tmExternalLeading;
                    LeadingSpace = metrics.tmInternalLeading;
                }
            }
        }

        private void SetFont(Font font)
        {
            this.Name = font.Name;
            this.Style = font.Style;
            this.Size = font.Size;
        }

        public Size MeasureString(string text, int charSpace, int maxChars)
        {
            if (text == null) text = string.Empty;
            int x = 0;
            foreach (char c in text)
            {
                if (--maxChars < 0) break;
                x += CharacterWidth(c) + charSpace;
            }
            return new Size(x, CharHeight);
        }

        public int CharacterWidth(char c)
        {
            CharData data;
            if (!Characters.TryGetValue(c, out data)) return 0;
            return data.Width;
        }

        public CharData GetCharData(char c)
        {
            CharData data;
            if (!Characters.TryGetValue(c, out data)) return CharData.Empty;
            return data;
        }


        public Size MeasureString(string text)
        {
            return MeasureString(text, 0, int.MaxValue);
        }

        private int GetTotalWidth()
        {
            int trail = (TrailingSpace + LeadingSpace) / 2;
            int sum = Characters.Count * trail;
            foreach (CharData w in Characters.Values) sum += w.Width;

            return sum;
        }

        private int CalculateIdealBitmapWidth()
        {
            int total = GetTotalWidth();
            int ideal = (int)Math.Ceiling(Math.Sqrt(total * CharHeight));

            return ideal;
            //return TextureUtil.ConvertToValidTextureDimension(ideal);
        }

        private int CalculateIdealBitmapHeight(int width)
        {
            int charHeight = CharHeight;
            int x = 0;
            int y = charHeight;

            int trail = (LeadingSpace + TrailingSpace) / 2;
            width -= trail;
            foreach(var v in Characters.Values)
            {
                int charWidth = v.Width;
                if ((x + charWidth) >= width)
                {
                    x = 0;
                    y += charHeight;
                }

                x += charWidth + trail;
            }
            return y;
            // return TextureUtil.ConvertToValidTextureDimension(y);
        }

        /// <summary>
        /// Generate a bitmap that contains an array of characters with a dimension of CharsPerRow * Rows.
        /// Each character is separated by MaxCharWidth.
        /// </summary>
        /// <returns>A bitmap with a character matrix.</returns>
        private void RenderBitmapAndFillCharArray(Texture texture, Font font)
        {
            int bmWidth = CalculateIdealBitmapWidth();

            bmWidth = TextureUtil.ConvertToValidTextureDimension(bmWidth);

            int bmHeight = CalculateIdealBitmapHeight(bmWidth);

            bmHeight = TextureUtil.ConvertToValidTextureDimension(bmHeight);

            int charHeight = CharHeight;

            TextureSize = new Size(bmWidth, bmHeight);

            float fWidth = (float)bmWidth;
            float fHeight = (float)bmHeight;

            int trail = (TrailingSpace + LeadingSpace) / 2;
            bool shadowed = IsShadowed;
            SolidBrush shadowBrush = shadowed ? new SolidBrush(Color.Black) : null;
            Color transparentColor = shadowed ? Color.Fuchsia : Color.Black;
            TextureFormat format = shadowed ? TextureFormat.LuminanceAlpha : TextureFormat.Alpha;
            //TextureFormat format = shadowed ? TextureFormat.LuminanceAlpha : TextureFormat.RGBA5551;
            using (DrawingCanvas canvas = texture.CreateCanvas(bmWidth, bmHeight, format, transparentColor))
            {
                Graphics g = canvas.Graphics;
                g.Clear(transparentColor);
                using (SolidBrush brush = new SolidBrush(Color.White))
                {
                    int x = 0;
                    int y = 0;
                    foreach (var pair in Characters)
                    {
                        char c = pair.Key;
                        CharData cd = pair.Value;
                        int charWidth = cd.Width;
                        if (charWidth > 0)
                        {
                            if ((x + charWidth) >= bmWidth)
                            {
                                x = 0;
                                y += charHeight;
                                if ((y + charHeight) >= bmHeight)
                                {
                                    y = bmHeight - charHeight;
                                }
                            }
                            string s = c.ToString();
                            if (shadowed) g.DrawString(s, font, shadowBrush, x + 1.5f, y + 1.5f);
                            g.DrawString(s, font, brush, x, y);
                        }

                        cd.X = (short)x;
                        cd.Y = (short)y;
                        x += charWidth + trail;
                    }
                }

            }
            if (shadowBrush != null) shadowBrush.Dispose();
        }


        ///// <summary>
        ///// Adds 4 pairs of x/y to the textureArrayPointer array that describe the
        ///// bottom/left - top/left - bottom/right - top/right of the character with the specified index in the bitmap.
        ///// </summary>
        ///// <param name="index">The character index</param>
        //private void AddCharacterArray(int charIndex, float left, float top, float right, float bottom)
        //{
        //    int aIndex = charIndex * 8;
        //    float[] a = textureArrayPointer;
        //    a[aIndex + 0] = left;
        //    a[aIndex + 1] = bottom;

        //    a[aIndex + 2] = left;
        //    a[aIndex + 3] = top;

        //    a[aIndex + 4] = right;
        //    a[aIndex + 5] = bottom;

        //    a[aIndex + 6] = right;
        //    a[aIndex + 7] = top;
        //}

        /// <summary>
        /// Gets a list of all lines wrapped due to the specified clip.
        /// </summary>
        /// <param name="text">Text to word wrap.</param>
        /// <param name="clip">Size that the text can occupy.</param>
        /// <param name="characterSpace">Space between each character.</param>
        /// <returns>List of string for each wrapped line.</returns>
        public IList<string> WordWrapLines(string text, Size clip, int characterSpace)
        {
            if (clip.Width < 0)
            {
                clip.Width = 0;
            }
            List<string> lines = new List<string>();
            int height = clip.Height;
            int h = 0;
            int ch = CharHeight;
            int max = clip.Width;
            if (text == null) text = string.Empty;
            bool appendLast = (text.Length > 0 && text[text.Length - 1] == '\n');
            while (!string.IsNullOrEmpty(text))
            {
                if ((h + ch) > height) break;
                int n = WordWrapLine(text, clip, characterSpace);
                if (n == 0) n++;
                //lines.Add(text.Substring(0, n).TrimEnd(' ', '\t', '\r'));
                //text = text.Remove(0, n).TrimStart(' ', '\t', '\r');
                lines.Add(text.Substring(0, n));
                text = text.Remove(0, n);

                h += ch;
            }
            if (appendLast) lines.Add(string.Empty);
            return lines;
        }

        private int WordWrapLine(string text, Size clipSize, int characterSpace)
        {
            int n = 0;
            int w = 0;
            int lastSpace = 0;
            int height = clipSize.Height;
            if (height == 0) height = int.MaxValue;
            int ch = CharHeight;
            int h = 0;
            foreach (char c in text)
            {
                if ((h + ch) > height) break;
                if (c == '\n') return n + 1;
                w += CharacterWidth(c) + characterSpace;
                if (w > clipSize.Width)
                {
                    return lastSpace > 0 ? lastSpace : n;
                }
                n++;
                if (IsSeparator(c)) lastSpace = n;
            }
            return n;
        }

        private bool IsSeparator(char c)
        {
            return "\t ,:;-+/\\~*!?=<>@".IndexOf(c) >= 0;
        }


        public IList<string> GetLines(string text, Size clipSize, bool ellipsis, int charSpace)
        {
            List<string> lines = new List<string>();
            {
                int height = clipSize.Height;
                if (height == 0) height = int.MaxValue;
                int h = 0;
                int ch = CharHeight;
                string[] split = text.Split('\n');
                foreach (string s in split)
                {
                    if ((h + ch) > height) break;
                    string line = s;
                    int w = MeasureStringInternal(ref line, clipSize.Width, charSpace, ellipsis);
                    lines.Add(line);
                    h += ch;
                }
                return lines;
            }
        }

        internal int MeasureStringInternal(ref string s, int maxWidth, int charSpace, bool ellipsis)
        {
            if (maxWidth == 0f) maxWidth = int.MaxValue;
            int ellipsisMax = ellipsis ? maxWidth - EllipsisWidth : maxWidth;
            int width = 0;
            int ellipsisLen = 0;
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                int charWidth = CharacterWidth(c);
                width += charWidth;
                if (width < ellipsisMax) ellipsisLen = i;
                if (width > maxWidth)
                {
                    width -= charWidth;
                    s = ellipsis ? s.Substring(0, ellipsisLen) + "..." : s.Substring(0, ellipsisLen);
                    return ellipsis ? width + EllipsisWidth : width;
                }
            }

            return width;
        }

        public int GetIndex(string s, int maxWidth, int charSpace, bool ellipsis)
        {
            if (maxWidth == 0f) maxWidth = int.MaxValue;
            int ellipsisMax = ellipsis ? maxWidth - EllipsisWidth : maxWidth;
            int width = 0;
            int ellipsisLen = 0;
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                int charWidth = CharacterWidth(c);
                width += charWidth;
                if (width < ellipsisMax) ellipsisLen = i;
                if (width > maxWidth)
                {
                    width -= charWidth;
                    s = ellipsis ? s.Substring(0, ellipsisLen) + "..." : s.Substring(0, ellipsisLen);
                    return i;
                }
            }

            return s.Length;

        }

        public int MeasureString(string s, int maxWidth, int charSpace, bool ellipsis)
        {
            return MeasureStringInternal(ref s, maxWidth, charSpace, ellipsis);
        }



        #endregion
        #region statics

        private static Dictionary<int, FontSprite> glFonts = new Dictionary<int, FontSprite>();

        public static FontSprite FromData(GlFontFamily fontFamily, int size, FontStyle style)
        {
            return FromData(fontFamily, size, style, false);
        }

        public static FontSprite FromFont(string name, int size, FontStyle style, string fileName, string chars)
        {
            string path = @"FontCache\" + fileName + size.ToString() + FontStyleToString(style, false) + ".glf";
            FontSprite font;
            font = FontSprite.FromFileOrNull(path, UriKind.Relative);
            if (font != null) return font;

            using (Font renderFont = new Font(name, size, style))
            {
                font = new FontSprite(renderFont, chars, false);

                if (isCacheEnabled)
                {
                    path = GetPhysicalFileName(path, UriKind.Relative);
                    string dir = Path.GetDirectoryName(path);
                    if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
                    font.WriteToFile(path, UriKind.Absolute);
                }
                return font;
            }
        }


        public static FontSprite FromFont(string name, int size, FontStyle style)
        {
            return FromFont(name, size, style, name, DefaultCharacters);
        }


        public static FontSprite FromData(GlFontFamily fontFamily, int size, FontStyle style, bool shadowed)
        {
            FontSprite font;

            int key = FontUtil.GetFontKey(fontFamily, size, style, shadowed);
            if (glFonts.TryGetValue(key, out font))
            {
                font.refCount++;
                return font;
            }

            string path = null;
            if (isCacheEnabled)
            {
                path = @"FontCache\" + fontFamily + size.ToString() + FontStyleToString(style, shadowed) + ".glf";
                font = FontSprite.FromFileOrNull(path, UriKind.Relative);
                if (font != null)
                {
                    font.key = key;
                    glFonts.Add(key, font);
                    return font;
                }
            }


            using (Font renderFont = FontUtil.FromData(fontFamily, size, style))
            {
                font = new FontSprite(renderFont, shadowed);
                font.key = key;
                glFonts.Add(key, font);

                if (isCacheEnabled)
                {
                    path = GetPhysicalFileName(path, UriKind.Relative);
                    string dir = Path.GetDirectoryName(path);
                    if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
                    font.WriteToFile(path, UriKind.Absolute);
                }
                return font;
            }
        }

        private static string FontStyleToString(FontStyle style, bool shadowed)
        {
            StringBuilder sb = new StringBuilder();
            if ((style & FontStyle.Bold) != 0) sb.Append('B');
            if ((style & FontStyle.Italic) != 0) sb.Append('I');
            if ((style & FontStyle.Underline) != 0) sb.Append('U');
            if ((style & FontStyle.Strikeout) != 0) sb.Append('S');
            if (shadowed) sb.Append("H");

            return sb.ToString();
        }

        public static bool IsNullOrDefault(FontSprite font)
        {
            if (font == null) return true;
            return font == regularFont;
        }

        #endregion
    }
}
